// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Cocoa/Cocoa.h>

#import "content/browser/accessibility/browser_accessibility_mac.h"

#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#import "content/browser/accessibility/browser_accessibility_cocoa.h"
#include "content/browser/accessibility/browser_accessibility_manager_mac.h"

namespace content {

// Static.
BrowserAccessibility* BrowserAccessibility::Create() {
  return new BrowserAccessibilityMac();
}

BrowserAccessibilityMac::BrowserAccessibilityMac()
    : browser_accessibility_cocoa_(NULL) {}

BrowserAccessibilityMac::~BrowserAccessibilityMac() {
  // Detach this object from |browser_accessibility_cocoa_| so it
  // no longer has a pointer to this object.
  [browser_accessibility_cocoa_ detach];

  // Now, release it - but at this point, other processes may have a
  // reference to the cocoa object.
  [browser_accessibility_cocoa_ release];
}

void BrowserAccessibilityMac::OnDataChanged() {
  BrowserAccessibility::OnDataChanged();

  if (browser_accessibility_cocoa_) {
    [browser_accessibility_cocoa_ childrenChanged];
    return;
  }

  // We take ownership of the Cocoa object here.
  browser_accessibility_cocoa_ =
      [[BrowserAccessibilityCocoa alloc] initWithObject:this];
}

// Replace a native object and refocus if it had focus.
// This will force VoiceOver to re-announce it, and refresh Braille output.
void BrowserAccessibilityMac::ReplaceNativeObject() {
  BrowserAccessibilityCocoa* old_native_obj = browser_accessibility_cocoa_;
  browser_accessibility_cocoa_ =
      [[BrowserAccessibilityCocoa alloc] initWithObject:this];

  // Replace child in parent.
  BrowserAccessibility* parent = PlatformGetParent();
  if (!parent)
    return;

  base::scoped_nsobject<NSMutableArray> new_children;
  NSArray* old_children = [ToBrowserAccessibilityCocoa(parent) children];
  for (uint i = 0; i < [old_children count]; ++i) {
    BrowserAccessibilityCocoa* child = [old_children objectAtIndex:i];
    if (child == old_native_obj)
      [new_children addObject:browser_accessibility_cocoa_];
    else
      [new_children addObject:child];
  }
  [ToBrowserAccessibilityCocoa(parent) swapChildren:&new_children];

  // If focused, fire a focus notification on the new native object.
  if (manager_->GetFocus() == this) {
    NSAccessibilityPostNotification(
        browser_accessibility_cocoa_,
        NSAccessibilityFocusedUIElementChangedNotification);
  }

  // Destroy after a delay so that VO is securely on the new focus first,
  // otherwise the focus event will not be announced.
  // We use 1000ms; however, this magic number isn't necessary to avoid
  // use-after-free or anything scary like that. The worst case scenario if this
  // gets destroyed, too early is that VoiceOver announces the wrong thing once.
  base::scoped_nsobject<BrowserAccessibilityCocoa> retained_destroyed_node(
      [old_native_obj retain]);

  base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(
          [](base::scoped_nsobject<BrowserAccessibilityCocoa> destroyed) {
            if (destroyed && [destroyed instanceActive]) {
              // Follow destruction pattern from NativeReleaseReference().
              [destroyed detach];
              [destroyed release];
            }
          },
          std::move(retained_destroyed_node)),
      base::TimeDelta::FromMilliseconds(1000));
}

uint32_t BrowserAccessibilityMac::PlatformChildCount() const {
  uint32_t child_count = BrowserAccessibility::PlatformChildCount();

  // If this is a table, include the extra fake nodes generated by
  // AXTableInfo, for the column nodes and the table header container, all of
  // which are only important on macOS.
  const std::vector<ui::AXNode*>* extra_mac_nodes = node()->GetExtraMacNodes();
  if (!extra_mac_nodes)
    return child_count;

  return child_count + extra_mac_nodes->size();
}

BrowserAccessibility* BrowserAccessibilityMac::PlatformGetChild(
    uint32_t child_index) const {
  if (child_index < BrowserAccessibility::PlatformChildCount())
    return BrowserAccessibility::PlatformGetChild(child_index);

  if (child_index >= PlatformChildCount())
    return nullptr;

  // If this is a table, include the extra fake nodes generated by
  // AXTableInfo, for the column nodes and the table header container, all of
  // which are only important on macOS.
  const std::vector<ui::AXNode*>* extra_mac_nodes = node()->GetExtraMacNodes();
  if (!extra_mac_nodes || extra_mac_nodes->empty())
    return nullptr;

  child_index -= BrowserAccessibility::PlatformChildCount();
  if (child_index < extra_mac_nodes->size())
    return manager_->GetFromAXNode((*extra_mac_nodes)[child_index]);

  return nullptr;
}

BrowserAccessibility* BrowserAccessibilityMac::PlatformGetFirstChild() const {
  return PlatformGetChild(0);
}

BrowserAccessibility* BrowserAccessibilityMac::PlatformGetLastChild() const {
  const std::vector<ui::AXNode*>* extra_mac_nodes = node()->GetExtraMacNodes();
  if (extra_mac_nodes && !extra_mac_nodes->empty())
    return manager_->GetFromAXNode(extra_mac_nodes->back());
  return BrowserAccessibility::PlatformGetLastChild();
}

BrowserAccessibility* BrowserAccessibilityMac::PlatformGetNextSibling() const {
  BrowserAccessibility* parent = PlatformGetParent();
  if (parent) {
    uint32_t next_child_index = node()->GetUnignoredIndexInParent() + 1;
    if (next_child_index >= parent->InternalChildCount() &&
        next_child_index < parent->PlatformChildCount()) {
      // get the extra_mac_node
      return parent->PlatformGetChild(next_child_index);
    } else if (next_child_index >= parent->PlatformChildCount()) {
      return nullptr;
    }
  }
  return BrowserAccessibility::PlatformGetNextSibling();
}

BrowserAccessibility* BrowserAccessibilityMac::PlatformGetPreviousSibling()
    const {
  BrowserAccessibility* parent = PlatformGetParent();
  if (parent) {
    uint32_t previous_child_index = node()->GetUnignoredIndexInParent() - 1;
    if (previous_child_index >= parent->InternalChildCount() &&
        previous_child_index < parent->PlatformChildCount()) {
      // get the extra_mac_node
      return parent->PlatformGetChild(previous_child_index);
    } else if (previous_child_index < 0) {
      return nullptr;
    }
  }
  return BrowserAccessibility::PlatformGetPreviousSibling();
}

const BrowserAccessibilityCocoa* ToBrowserAccessibilityCocoa(
    const BrowserAccessibility* obj) {
  DCHECK(obj);
  return static_cast<const BrowserAccessibilityMac*>(obj)->native_view();
}

BrowserAccessibilityCocoa* ToBrowserAccessibilityCocoa(
    BrowserAccessibility* obj) {
  DCHECK(obj);
  return static_cast<BrowserAccessibilityMac*>(obj)->native_view();
}

}  // namespace content
